/*
 * $RCSfile: ServiceInvocation.java,v $
 * $Revision: 1.1 $
 * $Date: 2010-06-21 $
 *
 * Copyright (C) 2008 Skin, Inc. All rights reserved.
 *
 * This software is the proprietary information of Skin, Inc.
 * Use is subject to license terms.
 */
package com.skin.demon;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.annotation.Resource;

import com.alibaba.fastjson.JSON;

/**
 * <p>Title: ServiceInvocation</p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2006</p>
 * @author xuesong.net
 * @version 1.0
 */
public class ServiceInvocation {
    private PrintWriter out;
    private BeanFactory beanFactory;

    /**
     * default
     */
    public ServiceInvocation() {
    }

    /**
     * @param beanFactory
     */
    public ServiceInvocation(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        this.out = new PrintWriter(System.out, true);
    }

    /**
     * @param beanFactory
     * @param out
     */
    public ServiceInvocation(BeanFactory beanFactory, PrintWriter out) {
        this.beanFactory = beanFactory;
        this.out = out;
    }

    /**
     * @param serviceName
     * @param methodName
     * @param args
     * @throws Exception
     */
    public void execute(String serviceName, String methodName, String[] args) throws Exception {
        Parameters parameters = new Parameters();
        parameters.setValue("serviceName", serviceName);
        parameters.setValue("methodName", methodName);

        if(args != null) {
            for(int i = 0; i < args.length; i++) {
                parameters.setValue("param" + i, args[i]);
            }
        }
        this.execute(parameters);
    }

    /**
     * @param parameters
     * @throws Exception
     */
    public void execute(Parameters parameters) throws Exception {
        String serviceName = parameters.getString("serviceName", "").trim();
        String methodName = parameters.getString("methodName", "").trim();
        String parameterTypeList = parameters.getString("parameterTypeList");

        if(serviceName.length() < 1) {
            this.info("serviceName must be not null !");
            return;
        }

        if(methodName.length() < 1) {
            this.info("methodName must be not null !");
            return;
        }

        this.info(serviceName + "." + methodName);

        try {
            Object service = null;
            Class<?> type = null;

            if(serviceName.startsWith("@")) {
                type = getClass(serviceName.substring(1));
            }
            else if(serviceName.startsWith("junit@")) {
                service = create(this.beanFactory, serviceName.substring(6));
                type = service.getClass();
            }
            else if(serviceName.startsWith("testng@")) {
                service = create(this.beanFactory, serviceName.substring(7));
                type = service.getClass();
            }
            else {
                service = this.beanFactory.getBean(serviceName);
                type = service.getClass();
            }

            Type[] parameterTypes = this.getParameterTypes(parameterTypeList);
            Class<?>[] interfaces = type.getInterfaces();
            Method method = null;

            this.info("Class: " + type.getName());
            this.info("Super interfaces:");

            for(int i = 0; i < interfaces.length; i++) {
                this.info(interfaces[i].getName());
            }

            if(parameterTypes != null) {
                /**
                 * 根据指定的方法重新获取泛型参数
                 */
                method = getMethod(type, methodName, parameterTypes);
                parameterTypes = method.getGenericParameterTypes();
            }
            else {
                method = this.getMethod(type, methodName);
                parameterTypes = method.getGenericParameterTypes();
            }

            this.info("Method Signature: " + getMethodSignature(method) + ";");

            for(int i = 0; i < parameterTypes.length; i++) {
                String name = "param" + i;
                this.info("parm" + i + ": " + parameters.getString(name));
            }

            this.info("=========================================");
            Object[] arguments = this.getArguments(parameters, parameterTypes);

            for(int i = 0; i < arguments.length; i++) {
                Object arg = arguments[i];
                Type clazz = parameterTypes[i];
                this.info("parm" + i + "[" + clazz.getClass().getName() + ": " + clazz.toString() + "]: " + arg);
            }

            Class<?> returnType = method.getReturnType();
            Object returnValue = method.invoke(service, arguments);
            String message = JSON.toJSONString(returnValue, true);

            if(returnType != null) {
                this.info("ReturnType: " + returnType.getName());
            }
            else {
                this.info("ReturnType: void");
            }
            this.info("ReturnValue: " + message);
        }
        catch(Throwable throwable) {
            throwable.printStackTrace(this.out);
            String stackTrace = this.getStackTrace(throwable);
            this.info("Exception: " + stackTrace);
        }
    }

    /**
     * @param beanFactory
     * @param className
     * @return Object
     * @throws Exception
     */
    public static Object create(BeanFactory beanFactory, String className) throws Exception {
        return create(beanFactory, getClass(className));
    }

    /**
     * @param beanFactory
     * @param type
     * @return T
     * @throws Exception 
     */
    public static <T> T create(BeanFactory beanFactory, Class<T> type) throws Exception {
        T t = type.newInstance();
        Field[] fields = getFields(type);

        if(fields != null) {
            for(Field field : fields) {
                if(Modifier.isStatic(field.getModifiers())) {
                    continue;
                }

                String name = getBeanName(field);

                if(name == null || name.trim().length() < 1) {
                    continue;
                }

                Object value = beanFactory.getBean(name);

                if(value != null) {
                    field.setAccessible(true);
                    field.set(t, value);
                }
            }
        }
        return t;
    }

    /**
     * @param type
     * @param methodName
     * @return Method
     */
    public Method getMethod(Class<?> type, String methodName) {
        Method[] methods = type.getMethods();

        for(Method method : methods) {
            if(methodName.equals(method.getName())) {
                return method;
            }
        }
        return null;
    }

    /**
     * @param methodName
     * @param parameterTypes
     * @return Method
     * @throws SecurityException
     * @throws NoSuchMethodException
     */
    private static Method getMethod(Class<?> type, String methodName, Type[] parameterTypes) throws NoSuchMethodException, SecurityException {
        Class<?>[] clazzs = new Class<?>[parameterTypes.length];

        for(int i = 0; i < parameterTypes.length; i++) {
            Type parameterType = parameterTypes[i];

            if(parameterType instanceof Class<?>) {
                clazzs[i] = (Class<?>)parameterType;
            }
        }
        return type.getMethod(methodName, clazzs);
    }

    /**
     * @param field
     * @return String
     */
    private static String getBeanName(Field field) {
        Annotation[] annotations = field.getAnnotations();

        for(Annotation annotation : annotations) {
            if(Resource.class.isAssignableFrom(annotation.getClass())) {
                String name = ((Resource)annotation).name();

                if(name == null || name.trim().length() < 1) {
                    return field.getName();
                }
                else {
                    return name;
                }
            }
            else if(isAssignableFrom(annotation, "org.springframework.beans.factory.annotation.Autowired")) {
                return field.getName();
            }
            else if(isAssignableFrom(annotation, "org.mockito.Mock")) {
                String name = null;

                try {
                    Method method = annotation.getClass().getDeclaredMethod("name", new Class<?>[0]);
                    name = (String)(method.invoke(annotation, new Object[0]));
                }
                catch (Exception e) {
                }

                if(name == null || name.trim().length() < 1) {
                    return field.getName();
                }
                else {
                    return name;
                }
            }
        }
        return null;
    }

    /**
     * @param object
     * @param className
     * @return boolean
     * @throws ClassNotFoundException
     */
    private static boolean isAssignableFrom(Object object, String className) {
        try {
            return getClass(className).isAssignableFrom(object.getClass());
        }
        catch (ClassNotFoundException e) {
        }
        return false;
    }

    /**
     * @param type
     * @param name
     * @return Field
     */
    public static Field getField(Class<?> type, String name) {
        Class<?> parent = type;

        while(true) {
            try {
                return type.getDeclaredField(name);
            }
            catch(Exception e) {
            }

            parent = parent.getSuperclass();

            if(parent == Object.class) {
                break;
            }
        }
        return null;
    }

    /**
     * @param type
     * @return Field[]
     */
    public static Field[] getFields(Class<?> type) {
        List<Class<?>> chain = new ArrayList<Class<?>>();
        Class<?> parent = type;

        while(true) {
            chain.add(parent);
            parent = parent.getSuperclass();

            if(parent == Object.class) {
                break;
            }
        }

        List<Field> result = new ArrayList<Field>();

        for(int i = chain.size() - 1; i > -1; i--) {
            Class<?> clazz = chain.get(i);
            Field[] fields = clazz.getDeclaredFields();

            for(Field field : fields) {
                result.add(field);
            }
        }

        Field[] array = new Field[result.size()];
        result.toArray(array);
        return array;
    }

    /**
     * @param parameterTypeList
     * @return Type<?>[]
     * @throws ClassNotFoundException
     */
    public Type[] getParameterTypes(String parameterTypeList) throws ClassNotFoundException {
        if(parameterTypeList != null) {
            String[] types = parameterTypeList.split(",");
            List<Type> parameterTypes = new ArrayList<Type>();

            if(types.length > 0) {
                for(String className : types) {
                    Class<?> clazz = getClass(className.trim());
                    parameterTypes.add(clazz);
                }

                Class<?>[] result = new Class<?>[parameterTypes.size()];
                parameterTypes.toArray(result);
                return result;
            }
            return new Class<?>[0];
        }
        return null;
    }

    /**
     * @param className
     * @return Class<?>
     * @throws ClassNotFoundException
     */
    public static Class<?> getClass(String className) throws ClassNotFoundException {
        if(className.equals("boolean")) {
            return boolean.class;
        }
        else if(className.equals("byte")) {
            return byte.class;
        }
        else if(className.equals("short")) {
            return short.class;
        }
        else if(className.equals("char")) {
            return char.class;
        }
        else if(className.equals("int")) {
            return int.class;
        }
        else if(className.equals("float")) {
            return float.class;
        }
        else if(className.equals("double")) {
            return double.class;
        }
        else if(className.equals("long")) {
            return long.class;
        }
        else if(className.equals("String")) {
            return String.class;
        }

        Class<?> clazz = null;

        try {
            clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
        }
        catch(Exception e) {
        }

        if(clazz == null) {
            try {
                clazz = ServiceInvocation.class.getClassLoader().loadClass(className);
            }
            catch(Exception e) {
            }
        }
        return Class.forName(className);
    }

    /**
     * @param method
     * @return String
     */
    public static String getMethodSignature(Method method) {
        Type[] parameterTypes = method.getGenericParameterTypes();
        Class<?> returnType = method.getReturnType();
        String modifiers = Modifier.toString(method.getModifiers());
        StringBuilder buffer = new StringBuilder();

        if(modifiers.length() > 0) {
            buffer.append(modifiers);
            buffer.append(" ");
        }

        buffer.append(returnType.getSimpleName());
        buffer.append(" ");
        buffer.append(method.getName());
        buffer.append("(");

        for(int i = 0, length = parameterTypes.length; i < length; i++) {
            Type type = parameterTypes[i];
            buffer.append(getTypeName(type));

            if(i < length - 1) {
                buffer.append(", ");
            }
        }
        buffer.append(")");
        return buffer.toString();
    }

    /**
     * @param method
     * @return String
     */
    public static String getMethodDescription(Method method) {
        Class<?> type = method.getDeclaringClass();
        Class<?>[] parameterTypes = method.getParameterTypes();
        StringBuilder buffer = new StringBuilder();

        buffer.append(type.getName());
        buffer.append(".");
        buffer.append(method.getName());
        buffer.append("(");

        for(int i = 0, length = parameterTypes.length; i < length; i++) {
            Class<?> clazz = parameterTypes[i];
            buffer.append(clazz.getName());

            if(i < length - 1) {
                buffer.append(", ");
            }
        }
        buffer.append(")");
        return buffer.toString();
    }

    /**
     * @param type
     * @return String
     */
    public static String getTypeName(Type type) {
        StringBuilder buffer = new StringBuilder();

        if(type instanceof Class) {
            return ((Class<?>)type).getSimpleName();
        }
        else if(type instanceof TypeVariable) {
            return ((TypeVariable<?>)type).getName();
        }
        else if(type instanceof WildcardType) {
            return ((WildcardType)type).toString();
        }
        else if(type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType)type;
            Type[] typeArguments = parameterizedType.getActualTypeArguments();

            for(Type t : typeArguments) {
                buffer.append(getTypeName(t));
                buffer.append(", ");
            }

            if(buffer.length() > 0) {
                buffer.delete(buffer.length() - 2, buffer.length());
            }

            Class<?> rawType = (Class<?>)(parameterizedType.getRawType());
    
            if(buffer.length() > 0) {
                buffer.insert(0, rawType.getSimpleName() + "<");
                buffer.append(">");
            }
            return buffer.toString();
        }
        else {
            return "unknown: " + type.toString();
        }
    }

    /**
     * @param parameters
     * @param parameterTypes
     * @return Object[]
     */
    @SuppressWarnings({"rawtypes","unchecked"})
    public Object[] getArguments(Parameters parameters, Type[] parameterTypes) {
        List<Object> arguments = new ArrayList<Object>();

        for(int i = 0; i < parameterTypes.length; i++) {
            String name = "param" + i;
            Type type = parameterTypes[i];
            Object value = null;

            if(type instanceof Class && ((Class)type).isEnum()) {
                Class<Enum> enumClass = (Class<Enum>)type;
                String enumName = parameters.getString(name);
                value = Enum.valueOf(enumClass, enumName);
            }
            else if(type == boolean.class) {
                value = parameters.getBoolean(name, false);
            }
            else if(type == Boolean.class) {
                value = parameters.getBoolean(name, null);
            }
            else if(type == byte.class) {
                value = parameters.getByte(name, (byte)0);
            }
            else if(type == Byte.class) {
                value = parameters.getByte(name, null);
            }
            else if(type == short.class) {
                value = parameters.getShort(name, (short)0);
            }
            else if(type == Short.class) {
                value = parameters.getShort(name, null);
            }
            else if(type == int.class) {
                value = parameters.getInteger(name, 0);
            }
            else if(type == Integer.class) {
                value = parameters.getInteger(name, null);
            }
            else if(type == float.class) {
                value = parameters.getFloat(name, 0.0f);
            }
            else if(type == Float.class) {
                value = parameters.getFloat(name, null);
            }
            else if(type == double.class) {
                value = parameters.getDouble(name, 0.0d);
            }
            else if(type == Double.class) {
                value = parameters.getDouble(name, null);
            }
            else if(type == long.class) {
                value = parameters.getLong(name, 0L);
            }
            else if(type == Long.class) {
                value = parameters.getLong(name, null);
            }
            else if(type == String.class) {
                value = parameters.getString(name, null);
            }
            else if(type == Date.class) {
                value = parameters.getDate(name);
            }
            else {
                String content = parameters.getString(name, null);

                if(content != null) {
                    value = JSON.parseObject(content, type);
                }
            }
            arguments.add(value);
        }

        Object[] result = new Object[arguments.size()];
        arguments.toArray(result);
        return result;
    }

    /**
     * @param content
     */
    public void info(String content) {
        this.out.println("[run] " + content);
    }

    /**
     * @param throwable
     * @return String
     */
    public String getStackTrace(Throwable throwable) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter printWriter = new PrintWriter(stringWriter);
        throwable.printStackTrace(printWriter);
        printWriter.flush();
        return stringWriter.toString();
    }

    /**
     * @return the beanFactory
     */
    public BeanFactory getBeanFactory() {
        return this.beanFactory;
    }

    /**
     * @param beanFactory the beanFactory to set
     */
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    /**
     * @return the out
     */
    public PrintWriter getOut() {
        return this.out;
    }

    /**
     * @param out the out to set
     */
    public void setOut(PrintWriter out) {
        this.out = out;
    }
}
